package com.hww.spring;


import com.hww.annotation.Autowired;
import com.hww.annotation.Component;
import com.hww.annotation.ComponentScan;
import com.hww.annotation.Scope;
import com.hww.bean.BeanDefinition;
import com.hww.bean.BeanPostProcessor;
import com.hww.bean.InitializingBean;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HwwApplicationContext {
    private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
    private Map<String, Object> singletonObjects = new HashMap<>();
    private List<BeanPostProcessor> beanPostProcessors = new ArrayList<>();
    private Map<String, Method> postConstructMethods = new HashMap<>();
    private Map<String, Method> preDestroyMethods = new HashMap<>();

    public HwwApplicationContext(Class<?> configClass) {
        if (configClass.isAnnotationPresent(ComponentScan.class)) {
            ComponentScan componentScan = configClass.getAnnotation(ComponentScan.class);
            String basePackage = componentScan.value();
            scan(basePackage);
        } else {
            throw new RuntimeException("Config class must be annotated with @ComponentScan");
        }
        //实例化
        createBeanInstances();
        //属性注入
        populateBeanProperties();
        //aware
        applyAware();
        //前置处理器
        applyBeanPostProcessorsBeforeInitialization();

        invokePostConstructMethods();
    }


    private void applyAware() {
        for (Map.Entry<String, Object> entry : singletonObjects.entrySet()) {
            String beanName = entry.getKey();
            Object bean = entry.getValue();

        }
    }

    private void scan(String basePackage) {
        String packagePath = basePackage.replace('.', '/');
        URL resource = Thread.currentThread().getContextClassLoader().getResource(packagePath);
        if (resource != null) {
            File dir = new File(resource.getFile());
            if (dir.exists()) {
                File[] files = dir.listFiles();
                if (files != null) {
                    for (File file : files) {
                        if (file.isDirectory()) {
                            scan(basePackage + "." + file.getName());
                        } else if (file.getName().endsWith(".class")) {
                            String className = basePackage + '.' + file.getName().replace(".class", "");
                            try {
                                Class<?> clazz = Class.forName(className);
                                if (clazz.isAnnotationPresent(Component.class)) {
                                    Component component = clazz.getAnnotation(Component.class);
                                    String beanName = component.value().isEmpty() ? lowerFirstCase(clazz.getSimpleName()) : component.value();
                                    BeanDefinition beanDefinition = new BeanDefinition(beanName, clazz);
                                    if (clazz.isAnnotationPresent(Scope.class)) {
                                        Scope scope = clazz.getAnnotation(Scope.class);
                                        beanDefinition.setScope(scope.value());
                                    }
                                    //添加BeanDefinition
                                    beanDefinitionMap.put(beanName, beanDefinition);
                                    //添加PostConstruct和PreDestroy方法
                                    findPostConstructAndPreDestroyMethods(beanName, clazz);
                                    //添加BeanPostProcessor
                                    if (BeanPostProcessor.class.isAssignableFrom(clazz)) {
                                        BeanPostProcessor beanPostProcessor = (BeanPostProcessor) clazz.getDeclaredConstructor().newInstance();
                                        beanPostProcessors.add(beanPostProcessor);
                                    }
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }

    private void findPostConstructAndPreDestroyMethods(String beanName, Class<?> clazz) {
        for (Method method : clazz.getMethods()) {
            if (method.isAnnotationPresent(PostConstruct.class)) {
                postConstructMethods.put(beanName, method);
            }
            if (method.isAnnotationPresent(PreDestroy.class)) {
                preDestroyMethods.put(beanName, method);
            }
        }
    }

    private void createBeanInstances() {
        for (Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet()) {
            String beanName = entry.getKey();
            BeanDefinition beanDefinition = entry.getValue();
            if ("singleton".equals(beanDefinition.getScope())) {
                Object bean = createBean(beanDefinition);
                singletonObjects.put(beanName, bean);
            }
        }
    }

    private Object createBean(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        try {
            Constructor<?> constructor = findSuitableConstructor(beanClass);
            Object bean = constructor.newInstance();

            return bean;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private Constructor<?> findSuitableConstructor(Class<?> beanClass) {
        Constructor<?>[] constructors = beanClass.getConstructors();
        if (constructors.length == 1) {
            return constructors[0];
        }
        for (Constructor<?> constructor : constructors) {
            if (constructor.isAnnotationPresent(Autowired.class)) {
                return constructor;
            }
        }
        return constructors[0];
    }

    private void applyBeanPostProcessorsBeforeInitialization() {
        for (Map.Entry<String, Object> entry : singletonObjects.entrySet()) {
            String beanName = entry.getKey();
            Object bean = entry.getValue();
            for (BeanPostProcessor processor : beanPostProcessors) {
                bean = processor.postProcessBeforeInitialization(bean, beanName);
            }
            if (bean instanceof InitializingBean) {
                try {
                    ((InitializingBean) bean).afterPropertiesSet();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            for (BeanPostProcessor processor : beanPostProcessors) {
                bean = processor.postProcessAfterInitialization(bean, beanName);
            }
            singletonObjects.put(beanName, bean);
        }
    }

    private void applyBeanPostProcessorsAfterInitialization() {
        for (Map.Entry<String, Object> entry : singletonObjects.entrySet()) {
            String beanName = entry.getKey();
            Object bean = entry.getValue();
            for (BeanPostProcessor processor : beanPostProcessors) {
                bean = processor.postProcessBeforeInitialization(bean, beanName);
            }
            if (bean instanceof InitializingBean) {
                try {
                    ((InitializingBean) bean).afterPropertiesSet();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            for (BeanPostProcessor processor : beanPostProcessors) {
                bean = processor.postProcessAfterInitialization(bean, beanName);
            }
            singletonObjects.put(beanName, bean);
        }
    }

    private void populateBeanProperties() {
        for (Map.Entry<String, Object> entry : singletonObjects.entrySet()) {
            String beanName = entry.getKey();
            Object bean = entry.getValue();
            Class<?> beanClass = bean.getClass();
            Field[] fields = beanClass.getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(Autowired.class)) {
                    String dependentBeanName = lowerFirstCase(field.getType().getSimpleName());
                    Object dependentBean = singletonObjects.get(dependentBeanName);
                    field.setAccessible(true);
                    try {
                        field.set(bean, dependentBean);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }


    private void invokePostConstructMethods() {
        for (Map.Entry<String, Method> entry : postConstructMethods.entrySet()) {
            String beanName = entry.getKey();
            Method postConstructMethod = entry.getValue();
            Object bean = singletonObjects.get(beanName);
            try {
                postConstructMethod.invoke(bean);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void destroyBeans() {
        for (Map.Entry<String, Method> entry : preDestroyMethods.entrySet()) {
            String beanName = entry.getKey();
            Method preDestroyMethod = entry.getValue();
            Object bean = singletonObjects.get(beanName);
            try {
                preDestroyMethod.invoke(bean);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private String lowerFirstCase(String str) {
        if (str == null || str.length() == 0) {
            return str;
        }
        return Character.toLowerCase(str.charAt(0)) + str.substring(1);
    }

    public Object getBean(String beanName) {
        return singletonObjects.get(beanName);
    }

    public <T> T getBean(Class<T> clazz) {
        for (Object bean : singletonObjects.values()) {
            if (clazz.isInstance(bean)) {
                return clazz.cast(bean);
            }
        }
        return null;
    }
}